<?php
namespace app\api\controller;
use think\Controller;
use think\Session;
use think\Db;
use think\cache\driver\Redis;
class Redistest{
    
    public function test(){
    	//会员名称
		$username = input('username');
		//手机号
		$mobile = input('mobile');
		//会员卡号
		$cardhyno = input('CardHyNo');
		//卡级
		$cardLevel = input('cardLevel');
		//券id 
		$couponId = input('couponId',1);
		//活动id 
		$action_id = input('actionId');
		//扣减积分值
		$score = input('score');
		//查券剩余库存
		$coupon = Db::name('coupon')
			->where('id',$couponId)
			->find();
    	$redisPackage = new Redis();
    	$setRedis = $redisPackage->set('coupon_id',$couponId);
    	$setRedis = $redisPackage->set('coupon_id',$couponId);
    	dump($setRedis);
    	$getRedis = $redisPackage->get('coupon_id');
    	dump($getRedis);
    	$lLenRedis = $redisPackage->lLen('coupon_id');
    	dump($lLenRedis);
    	
    	
    	$LPushRedis = $redisPackage->LPush('coupon_i',2,5,9);
    	dump($LPushRedis);
    	
    	$lPopRedis = $redisPackage->lPop('coupon_i',2,5,9);
    	dump($lPopRedis);
    	dump($coupon);
    }
    /*
     * 方案1
     * 
     * */
//  /**
//  * 访问产品前先将当前产品库存队列
//  * @access public
//  * @author bieanju
//  */
    public function _before_detail(){
        $where['id'] = $this->goods_id;
        $goods = Db::name("coupon")->where($where)->field('real_stock')->find();
        !$goods && $this->error("当前秒杀已结束！");
        if($goods['real_stock'] > $goods['total_stock']){
            $redis = $this->connectRedis();
            $getUserRedis = $redis->hGetAll("{$this->user_queue_key}");
            $gnRedis = $redis->llen("{$this->goods_number_key}");
            /* 如果没有会员进来队列库存 */
            if(!count($getUserRedis) && !$gnRedis){            
                for ($i = 0; $i < $goods['real_stock']; $i ++) {
                    $redis->lpush("{$this->goods_number_key}", 1);
                }
            }
            $resetRedis = $redis->llen("{$this->goods_number_key}");
            if(!$resetRedis){
                $this->error("系统繁忙，请稍后抢购！");
            }
        }else{
            $this->error("当前产品已经秒杀完！");
        }
         
    }
    /**
     * 抢购商品前处理当前会员是否进入队列
     * @access public
     * @author bieanju
     */
    public function goods_number_queue(){
        !$this->user_id && $this->ajaxReturn(array("status" => "-1","msg" => "请先登录"));
        $model = Db::name("coupon");
        $where['id'] = $this->goods_id;
        $goods_info = $model->where($where)->find();
        !$goods_info && $this->error("对不起当前商品不存在或已下架！"); 
        /* redis 队列 */  
        $redis = $this->connectRedis();
        /* 进入队列  */
        $goods_number_key = $redis->llen("{$this->goods_number_key}");
        if (!$redis->hGet("{$this->user_queue_key}", $this->user_id)) {
            $goods_number_key = $redis->lpop("{$this->goods_number_key}");
        }
         
        if($goods_number_key){
            // 判断用户是否已在队列
            if (!$redis->hGet("{$this->user_queue_key}", $this->user_id)) {
                // 插入抢购用户信息
                $userinfo = array(
                    "user_id" => $this->user_id,
                    "create_time" => time()
                );               
                $redis->hSet("{$this->user_queue_key}", $this->user_id, serialize($userinfo));
                $this->ajaxReturn(array("status" => "1"));
            }else{
                $modelCart = Db::name("cart");
                $condition['user_id'] = $this->user_id;
                $condition['id'] = $this->goods_id;
                $condition['prom_type'] = 1;
       			 $cartlist = $modelCart->where($condition)->count();
                if($cartlist > 0){
                    $this->ajaxReturn(array("status" => "2"));
                }else{
                  
                   $this->ajaxReturn(array("status" => "1"));
                  
                }
                 
            }
             
        }else{
            $this->ajaxReturn(array("status" => "-1","msg" => "系统繁忙,请重试！"));
        }
    }
    public function clearRedis(){
        set_time_limit(0);
        $redis = $this->connectRedis();
        //$Rd = $redis->del("{$this->user_queue_key}");
        $Rd = $redis->hDel("goods49",'用户id');
        $a = $redis->hGet("goods_49_user", '用户id');
        if(!$a){
            dump($a);
        }
         
        if($Rd == 0){
            exit("Redis队列已释放！");           
        }
    }
    public function test(){
    	$redisPackage = new RedisPackage();
    	$setRedis = $redisPackage->set('coupon',$couponId);
    	$setRedis = $redisPackage->set('coupon',$couponId);
    	dump($setRedis);
    	$getRedis = $redisPackage->get('coupon_id');
    	dump($getRedis);
    	$lLenRedis = $redisPackage->lLen('coupon_id');
    	dump($lLenRedis);
    	
    	
    	$LPushRedis = $redisPackage->LPush('coupon_i',2,5,9);
    	dump($LPushRedis);
    	$lPopRedis = $redisPackage->lPop('coupon_i',2,5,9);
    	dump($lPopRedis);
    }
     /*
     * 方案2
     * 初始化redis数据列表 模拟库存50，redis搭建在centos中已开启
     * */
    // 
//  秒杀的核心问题是在大并发的情况下不会超出库存的购买，这个就是处理的关键所以思路是第一步在秒杀类的先做一些基础的数据生成：
//	三张表做测试，分别是：商品表，日志表，订单表，
//	秒杀入口
    public function insva(){
        $id = input('id');//获取商品id
        if(!$id){
            return $this->insertlog(0);//记录失败日志
        }
        $redis = $this->redis();//接入redis
        $count = $redis->reduceStock('goods_stock');//减少库存，返回剩余库存
        if($count ==0){
            $this->insertlog(0);//记录秒杀失败日志
            return false;
        }else{
            $order = $this->build_order_no();//随机生成订单号
            $status = 1;
            $data = db('goods')->where('id',$id)->find();
            if (!$data){
                return $this->insertlog(0);//商品不存在
            }
            $res = db('order')->insert(['order_sn'=>$order,'uid'=>$this->user_id,'goods_id'=>$id]);//插入订单
            $stock = db('goods')->where('id',$id)->setDec('count');//减少库存
            if($stock){
                $this->insertlog();//记录成功日志
            }else{
                $this->insertlog(0);//记录秒杀失败日志
            }
        }
    }

    // 将商品库存存入队列
    public function redisinit(){
        $store=50; // 库存50
        $redis=$this->redis(); //接入redis
        $redis->del('goods_store'); // 删除库存列表
        $res=$redis->llen('goods_store'); //返回库存长度，这里已经是0
        $count=$store-$res;
        for($i=0;$i<$count;$i++){
            $redis->lpush('goods_store',1); //列表推进50个，模拟50个商品库存
        }
    }


	 
	//生成唯一订单
	public function build_order_no(){
	    return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
	}
	 
	// 记录日志 状态1成功 0失败
	public function insertlog($status=1){
	    return Db::name("apilog")->insertGetId(["count"=>1,"status"=>$status,"addtime"=>date('Y-m-d H:i:s')]);
	}
	
	
	/*
	        方案三
	 * 
	 * 
	 * 
	 * 
	 * */
	
	protected $table = "gopar_schedule_detail";  //数据表的
    protected $pk = "id"; //数据表的主键


//  public function _initialize()
//  {
//      //判断数据存在 并设置检查周期10分钟
//      if (!$this->checkLock("dataExists") && !$this->checkTableDataExists()){
//          throw  new  Exception("相关产品数据不存在");
//      }else{
//          //设置检查锁10分钟
//          $this->setLock("dataExists",600);
//      }
//      //如果数据不存在 初始化读取数据
//      if (!$this->checkExists()){
//          $this->initTableData();
//      }
//  }


    public function getScheduleCenter()
    {
        return Schedule::instance( $this->getInfoFieldValue("schedule_id"));
    }
    public function __destruct()
    {
        //设置15天自动回收redis
        $this->setExpire((int)$this->getScheduleCenter()->getInfoFieldValue("end_time")+3600*24*15);
    }
    public function index($data=["user_id"=>1,"ticket_detail_id"=>1,"buy_num"=>1]){
        try {
            //检测数据存在
            if (!$this->checkArrayValueEmpty($data,["user_id","ticket_detail_id","buy_num"])){
                throw  new  Exception($this->error);
            }

            $user_id= $data["user_id"] ;              //用户Id
            $ticket_detail_id = $data["ticket_detail_id"] ;  //产品Id
            $buy_num = $data["buy_num"] ;   //购买数量

            $infoCenter= ScheduleDetail::instance( $ticket_detail_id );
            $scheduleDetailInfo =$infoCenter->getInfoList();
            //修改数据库后 需要运行initTableData()方法重新初始化 推荐写到Hook里
           // $infoCenter->initTableData();
            if ( $infoCenter->getInfoFieldValue( "hot_schedule")){
                //热门抢购随机过滤随机过滤
                if (!in_array(rand(100, 200) % 11, [1, 3, 5, 7, 9])) {
                    throw  new  Exception("抢票人数众多 ,你被挤出抢购队伍,还有余票,请重新再抢");
                };
            }
            // 这里判断 购买数量和销售日期 不符合就  throw  new  Exception
            if (!true){
                throw  new  Exception("这里写不符合原因");
            }
            if (((int)$infoCenter->getInfoFieldValue("{$user_id}_num")+$buy_num)>$scheduleDetailInfo["limit_num"] ){
                throw  new  Exception("你超过最大购买数量");
            }
            if ($infoCenter->setInfoFieldIncre("pay_num",$buy_num) >$scheduleDetailInfo["limit_num"] ){
                //
                $infoCenter->setInfoFieldIncre("pay_num", -$buy_num);
                throw  new  Exception("对不起,票已经卖光了!");
            }
            //这里写主逻辑 启用事务功能创建订单 
            //事务参见下节源码
            
           
            //升级已销售数量
            $infoCenter->updateTableData(["pay_num"]);
            
            //在这里推荐埋钩子   处理订单完成的后续事情

             //返回结果

        } catch (Exception $e) {
            Log::error($e->getMessage());
            return ShowCode::jsonCodeWithoutData(1008, $e->getMessage());
        }
    }
    
     /** 方案4
      * 
      * tp框架开发的项目，需求是想控制某项业务同一时刻的访问量，用redis锁机制来实现目的，在tp公共方法中创建公共函数 concurrence
      * 
	  *redis控制并发
	**/
 	public function concurrence(){

	    //实例化redis
	    $redis = new \Redis();
	    $redis->connect(C('REDIS_HOST'),C('REDIS_PORT'));
	    $redis->auth(C('REDIS_AUTH'));
	
	    //进入队列及出列，queue在加减的时候，要保证操作的唯一性，此时加锁，完成后在解锁
	    $random = mt_rand();//生成随机数
	    $start = $redis->set('clock', $random, array('nx', 'ex' => 10));//redis加锁，锁有效期10秒
	    while (!$start){
	        usleep(100000);//沉睡100毫秒
	        $start = $redis->set('clock', $random, array('nx', 'ex' => 10));//redis加锁，锁有效期10秒
	    }
	    $queue = $redis->get('queue');
	    if ($queue <= 10){
	        $redis->incr('queue');//自增
	        if ($redis->get('clock') == $random) {//当clock值满足条件
	            $redis->del('clock');//删除$random随机数
	        }
	        $process = '处理具体的业务逻辑中,耗时不定,当前时间戳：'.time();
	
	        $endRandom = mt_rand();//随机数
	        $end = $redis->set('clock', $endRandom, array('nx', 'ex' => 10));//redis加锁，锁有效期10秒
	        while (!$end) {
	            usleep(100000);//沉睡100毫秒
	            $end = $redis->set('clock', $endRandom, array('nx', 'ex' => 10));//redis加锁，锁有效期10秒
	        }
	        $redis->decr('queue');//自减
	        if ($redis->get('clock') == $endRandom) {//当clock值满足条件
	            $redis->del('clock');//删除$endRandom随机数
	        }
	        return $process;
	    }else{
	        if ($redis->get('clock') == $random) {
	            $redis->del('clock');
	        }
	        usleep(200000);//沉睡200毫秒
	        concurrence();//递归再次调用
	    }
	}
	/*
	 * 方案5
	 * 
	 * PHP调用redis进去读写操作，大并发下会出现：读取key1，没有内容则写入内容，
	 * 但是大并发下会出现同时多个php进程写入的情况，
	 * 这个时候需要加一个锁，即获取锁的php进程有权限写。
	 * */
	public function addlock(){
		$lock_key = 'LOCK_PREFIX' . $redis_key;
		$is_lock = $redis->setnx($lock_key, 1); // 加锁
		if($is_lock == true){ // 获取锁权限
			$redis->setex($redis_key, $expire, $data); // 写入内容
			// 释放锁
			$redis->del($lock_key);
		}else{
			return true; // 获取不到锁权限，直接返回
		}
	}
	/*
	 * 方案5改进
	 * 
	 * 思路是：设置一个锁的key，setnx是原子操作，只能一个进程写入成功，写入成功返回true（表示获取锁权限），
	 * 然后写入内容再释放锁即删除锁key。
	 * 获取不到锁的进程直接返回。
	 * 但是这里有种情况，获取锁权限的进程，获取锁后运行报错了，导致没有释放锁，那么一直就不能写入内容，
	 * 这时就需要拿不到锁权限的进程去判断锁的剩余有效时间，
	 * 如果为-1则设置锁的有效时间为5秒（预留5秒给拿到锁的进程的运行时间，足够多了）。
	 * 改良后的代码：版权声明：本文为CSDN博主「CyborgLin」的原创文章，遵循CC 4.0 by-sa版权协议，转载请附上原文出处链接及本声明。
	 * 原文链接：https://blog.csdn.net/mxdzchallpp/article/details/68947509
	 * 
	 * */
	public function addlock1(){
		$lock_key = 'LOCK_PREFIX' . $redis_key;
		$is_lock = $redis->setnx($lock_key, 1); // 加锁
		if($is_lock == true){ // 获取锁权限
			$redis->setex($redis_key, $expire, $data); // 写入内容
			// 释放锁
			$redis->del($lock_key);
		}else{
			// 防止死锁
			if($redis->ttl($lock_key) == -1){
				$redis->expire($lock_key, 5);
			}
			return true; // 获取不到锁权限，直接返回
		}
	}
	/*
	 * 
	 * 这种情况看似没有什么问题，其实不然，大家注意我再设置所得时候，设置了一个过期时间，假如这个时间设置的是4秒，
	 * 那么如果进程A执行到删除前一刻一不小心超过了4秒，那么这个锁就自动消失了。
	 * 而另一个进程B查到没有锁，就加了一把自己的锁，此时进程A执行删除，就把B的锁给删除了（极小概率事件）。
	 * 这里解决方案有两种
	 * 1、设置比较长的expire时间，弊端：设置的太长，占用内存时间长，设置的太短不能完全解决问题。
	 * （可能有人会想不设置过期时间就可以，那么回到最初的错误点，如果程序设置了锁后崩溃了就变成了永久的锁。）
	 * 2、把对比和删除弄成一个原子操作，这里呢找到了一个方法，就是用redis的eval，把语句变成原子操作。
	 * 注意redis用的是lua语法
	 * */
	public function asslock2(){
	 	//同时设置val和过期时间，并使用setnx
	    $userId = 2;
	    $status = $this->redis->setnx($key,$userId,$expireTime);
	    if($status){
	         /**此处修改商品信息操作
	                ******
	        **/
	        //因为写这个博客的机器没有装redis，所以没有验证这个语法对不对。请大家见谅
	        $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
	        $result = $this->redis->eval(script,array($key,$val),1);
	        if ($result) {
	            return true;
	        }
	
	    }else{
	        echo '错误提示';
	    }
	}
	
	/**
     * 使用队列生成reids测试数据
     * 成功：执行 RPUSH操作后，返回列表的长度：8
     */
    public function createRedisList($listKey = 'message01'){
        $redis = RedisInstance::MasterInstance();
        $redis->select(1);
        $message = [
            'type' => 'say',
            'userId' => $redis->incr('user_id'),
            'userName' => 'Tinywan' . mt_rand(100, 9999), //是否正在录像
            'userImage' => '/res/pub/user-default-w.png', //是否正在录像
            'openId' => 'openId' . mt_rand(100000, 9999999999999999),
            'roomId' => 'openId' . mt_rand(30, 50),
            'createTime' => date('Y-m-d H:i:s', time()),
            'content' => $redis->incr('content') //当前是否正在打流状态
        ];
        $rPushResul = $redis->rPush($listKey, json_encode($message)); //执行成功后返回当前列表的长度 9
        return $rPushResul;
    }
     /**
     * 消息Redis方法保存到Mysql数据库
     * @param string $liveKey
     */
    public function RedisSaveToMysql($listKey = 'message01'){
        if (empty($listKey)) {
            $result = ["errcode" => 500, "errmsg" => "this parameter is empty!"];
            exit(json_encode($result));
        }
        $redis = RedisInstance::MasterInstance();
        $redis->select(1);
        $redisInfo = $redis->lRange($listKey, 0, 5);
        $dataLength = $redis->lLen($listKey);
        $model = M("User");
        while ($dataLength > 65970) {
            try {
                $model->startTrans();
                $redis->watch($listKey);
                $arrList = [];
                foreach ($redisInfo as $key => $val) {
                    $arrList[] = array(
                        'username' => json_decode($val, true)['userName'],
                        'logintime' => json_decode($val, true)['createTime'],
                        'description' => json_decode($val, true)['content'],
                        'pido' => json_decode($val, true)['content']
                    );
                }
                $insertResult = $model->addAll($arrList);
                if (!$insertResult) {
                    $model->rollback();
                    $result = array("errcode" => 500, "errmsg" => "Data Insert into Fail!", 'data' => 'dataLength:' . $dataLength);
                    exit(json_encode($result));
                }
                $model->commit();
                $redis->lTrim($listKey, 6, -1);
                $redisInfo = $redis->lRange($listKey, 0, 5);
                $dataLength = $redis->lLen($listKey);
            } catch (Exception $e) {
                $model->rollback();
                $result = array("errcode" => 500, "errmsg" => "Data Insert into Fail!");
                exit(json_encode($result));
            }
        }
        $result = array("errcode" => 200, "errmsg" => "Data Insert into Success!", 'data' => 'dataLength:' . $dataLength . 'liveKey:' . $listKey);
        exit(json_encode($result));
    }
}

